﻿// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using Silverlight.Windows.Controls.Primitives;

namespace Silverlight.Windows.Controls
{
    /// <summary>
    /// Represents a pop-up menu that enables a control to expose functionality that is specific to the context of the control.
    /// </summary>
    /// <QualityBand>Preview</QualityBand>
    public abstract class ContextMenuBase : MenuBase
    {
        /// <summary>
        /// Stores a reference to the current root visual.
        /// </summary>
        internal FrameworkElement _rootVisual;

        /// <summary>
        /// Stores a reference to the current window.
        /// </summary>
        internal Window _currentWindow;

        /// <summary>
        /// Stores the last known mouse position (via MouseMove).
        /// </summary>
        internal Point _mousePosition = new Point();

        /// <summary>
        /// Stores a reference to the object that owns the ContextMenu.
        /// </summary>
        internal DependencyObject _owner;

        /// <summary>
        /// Stores a reference to the current Popup.
        /// </summary>
        internal Popup _popup;

        /// <summary>
        /// Stores a reference to the current overlay.
        /// </summary>
        internal Panel _overlay;

        /// <summary>
        /// Stores a reference to the current Popup alignment point.
        /// </summary>
        internal Point _popupAlignmentPoint;

        /// <summary>
        /// Stores a value indicating whether the IsOpen property is being updated by ContextMenu.
        /// </summary>
        internal bool _settingIsOpen;

        /// <summary>
        /// Gets or sets the owning object for the ContextMenu.
        /// </summary>
        internal DependencyObject Owner
        {
            get { return _owner; }
            set
            {
                if (null != _owner)
                {
                    FrameworkElement ownerFrameworkElement = _owner as FrameworkElement;
                    if (null != ownerFrameworkElement)
                    {
                       ownerFrameworkElement.MouseRightButtonDown -= HandleOwnerMouseRightButtonDown;
                        ownerFrameworkElement.MouseRightButtonUp -= HandleOwnerMouseRightButtonUp;
                    }
                }
                _owner = value;
                if (null != _owner)
                {
                    FrameworkElement ownerFrameworkElement = _owner as FrameworkElement;
                    if (null != ownerFrameworkElement)
                    {
                       ownerFrameworkElement.MouseRightButtonDown += HandleOwnerMouseRightButtonDown;
                        ownerFrameworkElement.MouseRightButtonUp += HandleOwnerMouseRightButtonUp;
                    }
                }
            }
        }


        public static readonly DependencyProperty SidebarBackgroundProperty = DependencyProperty.Register(
            "SidebarBackground",
            typeof(Brush),
            typeof(ContextMenuBase),
            new PropertyMetadata(new SolidColorBrush(Color.FromArgb(0xff, 0xf1, 0xf1, 0xf1))));

        public Brush SidebarBackground
        {
           get { return (Brush)GetValue(SidebarBackgroundProperty); }
           set { SetValue(SidebarBackgroundProperty, value); }
        }

        public static readonly DependencyProperty SidebarBorderWidthProperty = DependencyProperty.Register(
            "SidebarBorderWidth",
            typeof(double),
            typeof(ContextMenuBase),
            new PropertyMetadata(1.0));

        public double SidebarBorderWidth
        {
           get { return (double)GetValue(SidebarBorderWidthProperty); }
           set { SetValue(SidebarBorderWidthProperty, value); }
        }

        /// <summary>
        /// Gets or sets the horizontal distance between the target origin and the popup alignment point.
        /// </summary>
        [TypeConverterAttribute(typeof(LengthConverter))]
        public double HorizontalOffset
        {
            get { return (double)GetValue(HorizontalOffsetProperty); }
            set { SetValue(HorizontalOffsetProperty, value); }
        }

        /// <summary>
        /// Identifies the HorizontalOffset dependency property.
        /// </summary>
        public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(
            "HorizontalOffset",
            typeof(double),
            typeof(ContextMenuBase),
            new PropertyMetadata(0.0, OnHorizontalVerticalOffsetChanged));

        /// <summary>
        /// Gets or sets the vertical distance between the target origin and the popup alignment point.
        /// </summary>
        [TypeConverterAttribute(typeof(LengthConverter))]
        public double VerticalOffset
        {
            get { return (double)GetValue(VerticalOffsetProperty); }
            set { SetValue(VerticalOffsetProperty, value); }
        }

        /// <summary>
        /// Identifies the VerticalOffset dependency property.
        /// </summary>
        public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register(
            "VerticalOffset",
            typeof(double),
            typeof(ContextMenuBase),
            new PropertyMetadata(0.0, OnHorizontalVerticalOffsetChanged));

        /// <summary>
        /// Handles changes to the HorizontalOffset or VerticalOffset DependencyProperty.
        /// </summary>
        /// <param name="o">DependencyObject that changed.</param>
        /// <param name="e">Event data for the DependencyPropertyChangedEvent.</param>
        private static void OnHorizontalVerticalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ContextMenuBase)o).UpdateContextMenuPlacement();
        }

        /// <summary>
        /// Gets or sets a value indicating whether the ContextMenu is visible.
        /// </summary>
        public bool IsOpen
        {
            get { return (bool)GetValue(IsOpenProperty); }
            set { SetValue(IsOpenProperty, value); }
        }

        /// <summary>
        /// Identifies the IsOpen dependency property.
        /// </summary>
        public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(
            "IsOpen",
            typeof(bool),
            typeof(ContextMenuBase),
            new PropertyMetadata(false, OnIsOpenChanged));

        /// <summary>
        /// Handles changes to the IsOpen DependencyProperty.
        /// </summary>
        /// <param name="o">DependencyObject that changed.</param>
        /// <param name="e">Event data for the DependencyPropertyChangedEvent.</param>
        private static void OnIsOpenChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((ContextMenuBase)o).OnIsOpenChanged((bool)e.OldValue, (bool)e.NewValue);
        }

        /// <summary>
        /// Handles changes to the IsOpen property.
        /// </summary>
        /// <param name="oldValue">Old value.</param>
        /// <param name="newValue">New value.</param>
        private void OnIsOpenChanged(bool oldValue, bool newValue)
        {
            if (!_settingIsOpen)
            {
                if (newValue)
                {
                    OpenPopup(_mousePosition);
                }
                else
                {
                    ClosePopup();
                }
            }
        }

        /// <summary>
        /// Occurs when a particular instance of a ContextMenu opens.
        /// </summary>
        public event RoutedEventHandler Opened;

        /// <summary>
        /// Called when the Opened event occurs.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnOpened(RoutedEventArgs e)
        {
            RoutedEventHandler handler = Opened;
            if (null != handler)
            {
                handler.Invoke(this, e);
            }
        }

        /// <summary>
        /// Occurs when a particular instance of a ContextMenu closes.
        /// </summary>
        public event RoutedEventHandler Closed;

        /// <summary>
        /// Called when the Closed event occurs.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnClosed(RoutedEventArgs e)
        {
            RoutedEventHandler handler = Closed;
            if (null != handler)
            {
                handler.Invoke(this, e);
            }
        }

        /// <summary>
        /// Called when the left mouse button is pressed.
        /// </summary>
        /// <param name="e">The event data for the MouseLeftButtonDown event.</param>
        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            e.Handled = true;
            base.OnMouseLeftButtonDown(e);
        }

        /// <summary>
        /// Called when the right mouse button is pressed.
        /// </summary>
        /// <param name="e">The event data for the MouseRightButtonDown event.</param>
        protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
        {
            e.Handled = true;
            base.OnMouseRightButtonDown(e);
        }

        /// <summary>
        /// Responds to the KeyDown event.
        /// </summary>
        /// <param name="e">The event data for the KeyDown event.</param>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Up:
                    FocusNextItem(false);
                    e.Handled = true;
                    break;
                case Key.Down:
                    FocusNextItem(true);
                    e.Handled = true;
                    break;
                case Key.Escape:
                    ClosePopup();
                    e.Handled = true;
                    break;
                // case Key.Apps: // Key.Apps not defined by Silverlight 4
            }
            base.OnKeyDown(e);
        }

        /// <summary>
        /// Supresses the MouseRightButtonDown event for the owning element.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Event arguments.</param>
        private void HandleOwnerMouseRightButtonDown
                (object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
        }

        /// <summary>
        /// Handles the MouseRightButtonUp event for the owning element.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Event arguments.</param>
        private void HandleOwnerMouseRightButtonUp
           (object sender, MouseButtonEventArgs e)
        {
           if (e.OriginalSource is DependencyObject)
           {
              if (Application.Current.IsRunningOutOfBrowser &&
                  Application.Current.HasElevatedPermissions)
              {
                 _currentWindow =
                     Window.GetWindow(e.OriginalSource as DependencyObject);
                 if (_currentWindow != null)
                 {
                    if (_currentWindow != Application.Current.MainWindow)
                    {
                       _rootVisual = _currentWindow.Content;
                    }
                    OpenPopup(e.GetPosition(null));
                 }
              }
              else
              {
                 _rootVisual = Application.Current.RootVisual as FrameworkElement;
                 OpenPopup(e.GetPosition(null));
              }
           }
           e.Handled = true; 
        }

        protected abstract object GetFocusedElement();

        /// <summary>
        /// Sets focus to the next item in the ContextMenu.
        /// </summary>
        /// <param name="down">True to move the focus down; false to move it up.</param>
        private void FocusNextItem(bool down)
        {
            int count = Items.Count;
            int startingIndex = down ? -1 : count;
            MenuItem focusedMenuItem = GetFocusedElement() as MenuItem;
            if (null != focusedMenuItem && (this == focusedMenuItem.ParentMenuBase))
            {
                startingIndex = ItemContainerGenerator.IndexFromContainer(focusedMenuItem);
            }
            int index = startingIndex;
            do
            {
                index = (index + count + (down ? 1 : -1)) % count;
                MenuItem container = ItemContainerGenerator.ContainerFromIndex(index) as MenuItem;
                if (null != container)
                {
                    if (container.IsEnabled && container.Focus())
                    {
                        break;
                    }
                }
            }
            while (index != startingIndex);
        }

        /// <summary>
        /// Called when a child MenuItem is clicked.
        /// </summary>
        internal void ChildMenuItemClicked()
        {
            ClosePopup();
        }

        /// <summary>
        /// Handles the SizeChanged event for the ContextMenu or RootVisual.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Event arguments.</param>
        internal void HandleContextMenuOrRootVisualSizeChanged(object sender, SizeChangedEventArgs e)
        {
            UpdateContextMenuPlacement();
        }

        /// <summary>
        /// Handles the MouseButtonDown events for the overlay.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Event arguments.</param>
        internal void HandleOverlayMouseButtonDown(object sender, MouseButtonEventArgs e)
        {
            ClosePopup();
            e.Handled = true;
        }

        /// <summary>
        /// Handles the MouseButtonDown events for the overlay.
        /// </summary>
        /// <param name="sender">Source of the event.</param>
        /// <param name="e">Event arguments.</param>
        internal void HandleOverlayRightMouseButtonDown(object sender, MouseButtonEventArgs e)
        {
            var p = e.GetPosition(null);
            var elements =
                Application.Current.IsRunningOutOfBrowser && Application.Current.HasElevatedPermissions
                ? VisualTreeHelper.FindElementsInHostCoordinates(p, Window.GetWindow(e.OriginalSource as DependencyObject))
                : VisualTreeHelper.FindElementsInHostCoordinates(p, Application.Current.RootVisual);
            if (elements.Contains(_owner))
            {
                // Handle second right click on same owner control
                HandleOwnerMouseRightButtonDown(sender, e);
            }
            else
            {
                ClosePopup();
                e.Handled = true;
            }
        }

        /// <summary>
        /// Updates the location and size of the Popup and overlay.
        /// </summary>
        protected abstract void UpdateContextMenuPlacement();

        /// <summary>
        /// Opens the Popup.
        /// </summary>
        /// <param name="position">Position to place the Popup.</param>
        protected abstract void OpenPopup(Point position);

        /// <summary>
        /// Closes the Popup.
        /// </summary>
        protected abstract void ClosePopup();
    }
}
